package checker

import (
	"kumachan/interpreter/lang/textual/ast"
	"kumachan/interpreter/lang/common/source"
	"kumachan/interpreter/compiler/checker/checked"
	"kumachan/interpreter/compiler/checker/typsys"
)


type overloadOption struct {
	function  *Function
	value     *checked.Expr
}
type overloadCandidate struct {
	function  *Function
	error     *source.Error
}

func callExpr(cc *checkContext, callee ast.Expr, arg ast.Expr) checkResult {
	var call_lambda, is_lambda = getLambdaCaller(callee)
	if is_lambda {
		return call_lambda(cc, arg)
	}
	var callee_expr, err1 = cc.checkChildExpr(nil, callee)
	if err1 != nil { return cc.propagate(err1) }
	var io, callable = cc.unboxLambda(callee_expr)
	if !(callable) {
		return cc.propagate(source.MakeError(callee.Location,
			E_TypeNotCallable {
				TypeName: typsys.DescribeType(callee_expr.Type, nil),
			}))
	}
	var in = io.Input
	var out = io.Output
	var arg_expr, err2 = cc.checkChildExpr(in, arg)
	if err2 != nil { return cc.propagate(err2) }
	return cc.assign(
		out,
		checked.Call {
			Callee:   callee_expr,
			Argument: arg_expr,
		})
}
func callLocalRef(cc *checkContext, ref LocalRef, params ([] typsys.Type), ref_loc source.Location, arg ast.Expr) checkResult {
	if len(params) > 0 {
		return cc.propagate(source.MakeError(ref_loc,
			E_TypeParametersOnLocalBindingRef {}))
	}
	var ref_type = ref.Binding.Type
	var io, callable = cc.unboxLambdaFromType(ref_type)
	if !(callable) {
		return cc.propagate(source.MakeError(ref_loc,
			E_TypeNotCallable {
				TypeName: typsys.DescribeType(ref_type, nil),
			}))
	}
	var in = io.Input
	var out = io.Output
	var ref_expr = &checked.Expr {
		Type:    ref_type,
		Info:    checked.ExprInfoFrom(ref_loc),
		Content: checked.LocalRef { Binding: ref.Binding },
	}
	var arg_expr, err2 = cc.checkChildExpr(in, arg)
	if err2 != nil { return cc.propagate(err2) }
	return cc.assign(
		out,
		checked.Call {
			Callee:   ref_expr,
			Argument: arg_expr,
		})
}
func callFuncRefs (
	cc      *checkContext,
	callee  FuncRefs,
	params  [] typsys.Type,
	arg     ast.Expr,
	pivot   typsys.Type,
) checkResult {
	if len(callee.Functions) == 0 { panic("something went wrong") }
	var raw_arg = arg
	var possibly_checked_arg, err = (func() (ast.Expr, *source.Error) {
		var _, is_record = getPlainRecordTerm(raw_arg)
		if is_record {
			return raw_arg, nil
		}
		var arg_expr, err = cc.checkChildExpr(nil, raw_arg)
		if err == nil {
			return ast.WrapTermAsExpr(MakeCheckedExprTerm(arg_expr)), nil
		} else {
			switch err.Content.(type) {
			case E_ExplicitTypeRequired, E_AmbiguousFunctionUsage:
				return raw_arg, nil
			default:
				return ast.Expr {}, err
			}
		}
	})()
	if err != nil { return cc.propagate(err) }
	arg = possibly_checked_arg
	var ctx = cc.exprContext
	var loc = cc.location
	var options = make([] overloadOption, 0)
	var candidates = make([] overloadCandidate, 0)
	for _, f := range callee.Functions {
		var sig = cloneSignatureFrom(f)
		var params_def = sig.TypeParameters
		var io = sig.InputOutput
		var in_t = io.Input
		var out_t = io.Output
		var trial = cc.copy()
		var result = trial.infer(params_def, params, out_t, func(s0 *typsys.InferringState) (checked.ExprContent, *typsys.InferringState, *source.Error) {
			var arg_expr, s1, err = check(arg)(in_t, s0, ctx)
			if err != nil { return nil, nil, err }
			var a1 = typsys.MakeAssignContext(ctx.ModName, s1)
			var ok, s2 = typsys.Assign(in_t, arg_expr.Type, a1)
			if !(ok) { return nil, nil, source.MakeError(arg.Location,
				E_NotAssignable {
					From: typsys.DescribeType(arg_expr.Type, nil),
					To:   typsys.DescribeType(in_t, s1),
				}) }
			var n = f.Name
			{ var f_expr, s3, err = makeFuncRef(n, sig, s2, pivot, loc, ctx)
			if err != nil { return nil, nil, err }
			return checked.Call {
				Callee:   f_expr,
				Argument: arg_expr,
			}, s3, nil }
		})
		if result.err != nil {
			var err = result.err
			candidates = append(candidates, overloadCandidate {
				function: f,
				error:    result.err,
			})
			// overload error: cannot nest (time complexity), must propagate
			switch err.Content.(type) {
			case E_InvalidFunctionUsage, E_AmbiguousFunctionUsage:
				return cc.propagate(err)
			}
		} else {
			options = append(options, overloadOption {
				function: f,
				value:    result.expr,
			})
		}
	}
	{ var expr, err = decideOverload(options, candidates, loc)
	if err != nil { return cc.propagate(err) }
	return cc.assignExpr(expr) }
}
func callConstructor (
	cc      *checkContext,
	callee  *typsys.TypeDef,
	params  [] typsys.Type,
	arg     ast.Expr,
) checkResult {
	switch D := callee.Content.(type) {
	case *typsys.Box:
		var ctx_mod = cc.exprContext.ModName
		if ctx_mod != callee.Name.ModuleName {
			switch D.BoxKind {
			case typsys.Protected:
				return cc.error(
					E_BoxIntoProtected {
						TypeName:   callee.Name.String(),
						CtxModName: ctx_mod,
					})
			case typsys.Opaque:
				return cc.error(
					E_BoxIntoOpaque {
						TypeName:   callee.Name.String(),
						CtxModName: ctx_mod,
					})
			default:
				// OK
			}
		}
		var ctx = cc.exprContext
		var params_def = cloneParameters(callee.Parameters)
		var replace = parameterReplacer(callee.Parameters, params_def)
		var in_t = replace(D.InnerType)
		var out_t = replace(defType(callee))
		return cc.infer(params_def, params, out_t, func(s0 *typsys.InferringState) (checked.ExprContent, *typsys.InferringState, *source.Error) {
			var arg_expr, s1, err = check(arg)(in_t, s0, ctx)
			if err != nil { return nil, nil, err }
			return checked.Wrapper { Inner: arg_expr }, s1, nil
		})
	case *typsys.Enum:
		if len(params) > 0 { return cc.error(E_RedundantTypeParameters {}) }
		var arg_expr, err = cc.checkChildExpr(nil, arg)
		if err != nil { return cc.propagate(err) }
		var expr, ok = liftToEnum(arg_expr, callee)
		if !(ok) {
			return cc.error(
				E_InvalidConstructorCall {})
		}
		return cc.assignExpr(expr)
	case *typsys.Interface:
		if len(params) > 0 { return cc.error(E_RedundantTypeParameters {}) }
		var arg_expr, err = cc.checkChildExpr(nil, arg)
		if err != nil { return cc.propagate(err) }
		var arg_t = arg_expr.Type
		var arg_def, type_args, ok = typeGetRefDefArgs(arg_t)
		if !(ok) {
			return cc.error(
				E_InvalidConstructorCall {})
		}
		var callee_t = &typsys.NestedType { Content:
			typsys.Ref {
				Def:  callee,
				Args: type_args,
			} }
		var _, arg_is_interface = arg_def.Content.(*typsys.Interface)
		if arg_is_interface {
			// interface -> interface
			var path, ok = typeDefFindPathToAncestor(arg_def, callee, nil)
			if !(ok) {
				return cc.error(
					E_NotImplementation {
						Type:      cc.describeType(arg_t),
						Interface: cc.describeType(callee_t),
					})
			}
			return cc.assign(
				callee_t,
				checked.InterfaceTransform {
					Argument: arg_expr,
					Path:     path,
				})
		} else {
			// concrete -> interface
			var pair = ImplPair {
				ConcreteType:  arg_def,
				InterfaceType: callee,
			}
			var table, ok = cc.exprContext.DispatchMapping[pair]
			if !(ok) {
				return cc.error(
					E_NotImplementation {
						Type:      cc.describeType(arg_t),
						Interface: cc.describeType(callee_t),
					})
			}
			return cc.assign(
				callee_t,
				checked.Interface {
					ConcreteValue: arg_expr,
					DispatchTable: table,
				})
		}
	default:
		return cc.error(
			E_InvalidConstructor {})
	}
}

func getLambdaCaller(callee ast.Expr) (func(cc *checkContext, arg ast.Expr)(checkResult), bool) {
	switch T := callee.Term.Term.(type) {
	case ast.Lambda:
		return func(cc *checkContext, arg ast.Expr) checkResult {
			return callLambda(cc, T, arg)
		}, true
	case ast.PipelineLambda:
		return func(cc *checkContext, arg ast.Expr) checkResult {
			var f = quickPipelineLambda(T)
			return callQuickLambda(cc, f, arg)
		}, true
	case ast.ConstructorLambda:
		return func(cc *checkContext, arg ast.Expr) checkResult {
			var f, err = quickConstructorLambda(T, cc.typeConsContext())
			if err != nil { return cc.propagate(err) }
			return callQuickLambda(cc, f, arg)
		}, true
	default:
		return nil, false
	}
}
func callLambda(cc *checkContext, callee ast.Lambda, arg ast.Expr) checkResult {
	// transform into a let expression
	var B = ast.Block {
		Node:     callee.Node,
		Bindings: [] ast.Binding { {
			Node:    callee.Node,
			Pattern: callee.Input,
			Value:   arg,
		} },
		Return:   callee.Output,
	}
	return cc.forward(checkBlock(B))
}
func callQuickLambda(cc *checkContext, callee quickLambda, arg ast.Expr) checkResult {
	return cc.forwardToChildExpr(callee(arg))
}

func checkCallPrefix(callee ast.Expr, arg ast.Expr, loc source.Location) ExprChecker {
	return makeExprChecker(loc, func(cc *checkContext) checkResult {
		var ref_node, is_ref = getInlineRef(callee)
		if is_ref {
			var ref, params, err = cc.resolveInlineRef(ref_node, nil)
			if err != nil { return cc.propagate(err) }
			var ref_loc = ref_node.Location
			switch R := ref.(type) {
			case FuncRefs:
				return callFuncRefs(cc, R, params, arg, nil)
			case LocalRef:
				return callLocalRef(cc, R, params, ref_loc, arg)
			case LocalRefWithFuncRefs:
				// a local binding shadows global functions in a prefix call
				return callLocalRef(cc, R.LocalRef, params, ref_loc, arg)
			default:
				panic("impossible branch")
			}
		} else {
			return callExpr(cc, callee, arg)
		}
	})
}
func checkCallPipe(callee ast.Expr, left *checked.Expr, right ast.MaybeExpr, loc source.Location) ExprChecker {
	return makeExprChecker(loc, func(cc *checkContext) checkResult {
		if left == nil { panic("something went wrong") }
		var arg = (func() (ast.Expr) {
			var node_left = ast.WrapTermAsExpr(MakeCheckedExprTerm(left))
			var node_right, ok = right.(ast.Expr)
			if ok {
				var pair = [] ast.Expr { node_left, node_right }
				return craftNodeTuple(pair, node_right.Node)
			} else {
				return node_left
			}
		})()
		var ref_node, is_ref = getInlineRef(callee)
		if is_ref {
			var ref, params, err = cc.resolveInlineRef(ref_node, left.Type)
			if err != nil { return cc.propagate(err) }
			var ref_loc = ref_node.Location
			switch R := ref.(type) {
			case FuncRefs:
				return callFuncRefs(cc, R, params, arg, left.Type)
			case LocalRef:
				return callLocalRef(cc, R, params, ref_loc, arg)
			case LocalRefWithFuncRefs:
				// global functions are preferred in a pipeline or infix call
				return callFuncRefs(cc, R.FuncRefs, params, arg, left.Type)
			default:
				panic("something went wrong")
			}
		} else {
			return callExpr(cc, callee, arg)
		}
	})
}
func checkCallInfix(operator ast.Expr, left ast.Expr, right ast.Expr, loc source.Location) ExprChecker {
	return makeExprChecker(loc, func(cc *checkContext) checkResult {
		var left_expr, err = cc.checkChildExpr(nil, left)
		if err != nil { return cc.propagate(err) }
		return cc.forward(checkCallPipe(operator, left_expr, right, loc))
	})
}
func checkCallCps(callee ast.Expr, arg ast.Expr, k_in ast.MaybeCpsBinding, k_out ast.Expr, node ast.Node) ExprChecker {
	return makeExprChecker(node.Location, func(cc *checkContext) checkResult {
		var k = ast.WrapTermAsExpr(ast.VariousTerm {
			Node: node,
			Term: desugarCpsLambda(k_in, k_out, node),
		})
		return cc.forward(checkCallInfix(callee, arg, k, node.Location))
	})
}

func decideOverload (
	options     ([] overloadOption),
	candidates  ([] overloadCandidate),
	location    source.Location,
) (*checked.Expr, *source.Error) {
	if len(options) == 0 {
		if len(candidates) == 0 {
			panic("something went wrong")
		}
		if len(candidates) == 1 {
			return nil, candidates[0].error
		}
		return nil, source.MakeError(location,
			E_InvalidFunctionUsage {
				Candidates: describeOverloadCandidates(candidates),
			})
	} else if len(options) == 1 {
		return options[0].value, nil
	} else {
		var output_types = make([] typsys.Type, len(options))
		for i, option := range options {
			output_types[i] = option.function.Signature.InputOutput.Output
		}
		var min_index, found = findUniqueMinimumNamedType(output_types)
		if !(found) {
			return nil, source.MakeError(location,
				E_AmbiguousFunctionUsage {
					Options: describeOverloadOptions(options),
				})
		}
		return options[min_index].value, nil
	}
}
func findUniqueMinimumNamedType(types ([] typsys.Type)) (int, bool) {
	if len(types) < 2 {
		panic("invalid argument")
	}
	var defs = make([] *typsys.TypeDef, len(types))
	for i, t := range types {
		var def, ok = typeGetRefDef(t)
		if !(ok) { return -1, false }
		defs[i] = def
	}
	var min = defs[0]
	var min_index = 0
	for i, d := range defs {
		if i == 0 {
			continue
		}
		if typeDefSubtypingOrderLessThanOperator(d, min) {
			min = d
			min_index = i
		} else if typeDefSubtypingOrderLessThanOperator(min, d) {
			// do nothing
		} else if min == d {
			// do nothing
		} else {
			// not comparable
			return -1, false
		}
	}
	for i, d := range defs {
		if i != min_index && d == min {
			// not unique
			return -1, false
		}
	}
	return min_index, true
}
func describeOverloadOptions(options ([] overloadOption)) ([] OverloadOptionInfo) {
	var info = make([] OverloadOptionInfo, len(options))
	for i, option := range options {
		info[i] = OverloadOptionInfo {
			Name:      option.function.Name.Name.String(),
			Signature: describeFunctionSignature(option.function.Signature),
		}
	}
	return info
}
func describeOverloadCandidates(candidates ([] overloadCandidate)) ([] OverloadCandidateInfo) {
	var info = make([] OverloadCandidateInfo, len(candidates))
	for i, candidate := range candidates {
		info[i] = OverloadCandidateInfo {
			Name:      candidate.function.Name.Name.String(),
			Signature: describeFunctionSignature(candidate.function.Signature),
			Error:     candidate.error,
		}
	}
	return info
}

func cloneParameters(p ([] typsys.Parameter)) ([] typsys.Parameter) {
	var clone = make([] typsys.Parameter, len(p))
	for i := range p {
		clone[i] = p[i]
	}
	return clone
}
func parameterReplacer(from ([] typsys.Parameter), to ([] typsys.Parameter)) func(typsys.Type)(typsys.Type) {
	return func(t typsys.Type) typsys.Type {
		return typsys.TypeOpParameterReplace(from, to, t)
	}
}
func cloneSignatureFrom(f *Function) (FunctionSignature) {
	var declared = f.Signature.TypeParameters
	var clone = cloneParameters(declared)
	var replace = parameterReplacer(declared, clone)
	var ic_t = &typsys.NestedType { Content: f.Signature.ImplicitContext }
	var io_t = &typsys.NestedType { Content: f.Signature.InputOutput }
	var ic_clone, ok1 = getRecord(replace(ic_t))
	if !(ok1) { panic("something went wrong") }
	var io_clone, ok2 = getLambda(replace(io_t))
	if !(ok2) { panic("something went wrong") }
	return FunctionSignature {
		TypeParameters:  clone,
		ImplicitContext: ic_clone,
		InputOutput:     io_clone,
	}
}

func getInlineRef(expr ast.Expr) (ast.InlineRef, bool) {
	if len(expr.Pipeline) == 0 {
		var inline_ref, ok = expr.Term.Term.(ast.InlineRef)
		return inline_ref, ok
	} else {
		return ast.InlineRef {}, false
	}
}
func craftNodeTuple(elements ([] ast.Expr), node ast.Node) ast.Expr {
	return ast.WrapTermAsExpr(ast.VariousTerm {
		Node: node,
		Term: ast.Tuple {
			Node:     node,
			Elements: elements,
		},
	})
}
func desugarCpsLambda(in ast.MaybeCpsBinding, out ast.Expr, node ast.Node) ast.Lambda {
	var pattern = (func() ast.VariousPattern {
		var cps_bind, ok = in.(ast.CpsBinding)
		if ok {
			return cps_bind.Pattern
		} else {
			var pattern_unit = ast.VariousPattern {
				Node:    node,
				Pattern: ast.PatternTuple {
					Node:  node,
					Names: [] ast.Identifier {},
				},
			}
			return pattern_unit
		}
	})()
	return ast.Lambda {
		Node:   node,
		Input:  pattern,
		Output: out,
	}
}


